<?php
/**
 * This file houses the ClassReflector class defintion.
 *
 * Copyright (c) 2010, Kalkomey Enterprises, Inc.
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * - Redistributions of source code must retain the above copyright notice, this list
 *   of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice, this 
 *   list of conditions and the following disclaimer in the documentation and/or other
 *   materials provided with the distribution.
 * - Neither the name of the Kalkomey Enterprises, Inc. nor the names of its 
 *   contributors may be used to endorse or promote products derived from this 
 *   software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
 * @copyright Copyright (c) 2010, Kalkomey Enterprises, Inc.
 * @link http://code.google.com/p/swerv
 * @package Framework
 * @subpackage Library
 */

/**
 * The ClassReflector object can be used to reflect any other class.  It returns an array containing full information about methods and properties defined.
 *
 * @package Framework
 * @subpackage Library
 */
class ClassReflector {

	/** 
	 * The location of the class being reflected.  This must be known as the class must be loaded in order for reflection to work.  Usually this is automatically determined.
	 * @var string
	 */ 
	private $classFile;

	/** 
	 * The location of the class being reflected.  This must be known as the class must be loaded in order for reflection to work.  Usually this is automatically determined.
	 * @var string
	 */ 
	private $className;

	/** 
	 * The name of the class being reflected.
	 * @var string
	 */ 
	private $reflection;

	/** 
	 * This holds the reflected data.
	 * @var string
	 */ 
	private $data;
	
	/**
	 * Object constructor.
	 *
	 * @uses Handler::findClassPath()
	 *
	 * @throws Exception if unable to find the class
	 *
	 * @param string $className the name of the class to reflect
	 * @param string $classFile the location of the class being reflected; usually this is auto-determined, but if not, you should provide it
	 *
	 * @return ClassReflector
	 */
	public function __construct($className, $classFile = null) {
		if ($classFile === null) {
			$classFile = Handler::findClassPath($className);
		}
		if ($classFile === null || $classFile === false) {
			throw new Exception('Unable to find ' . $className);
		}
		$this->classFile = $classFile;
		$this->className = $className;
		$this->data = array();
	}

	/**
	 * Reflects the class and returns a multi-diminsional associative array that contains all of the methods and properties of the class as well as descriptions and types if provided in DocBlock comments.
	 *
	 * @uses ClassReflector::reflectProperties()
	 * @uses ClassReflector::reflectMethods()
	 *
	 * @return array
	 */
	public function reflect() {
		if (!class_exists($this->className, false)) {
			require($this->classFile);
		}
		$this->reflection = new ReflectionClass($this->className);
		$comments = $this->reflection->getDocComment();
		$this->data['description'] = '';
		$this->data['is_public'] = false;
		$this->data['name'] = $this->className;
		if (strlen($comments) > 0) {
			$lines = explode("\n", $comments);
			foreach ($lines as $line) {
				$line = trim($line);
				$line = substr($line, 2, strlen($line));
				if ($line == '@exposePublic') {
					$this->data['is_public'] = true;
				} elseif (substr($line, 0, 1) == '@') {
					continue;
				} elseif ($line != '*') {
					$this->data['description'] .= trim($line) . "\n";
				}
			}
			$this->data['description'] = trim($this->data['description']);
		}
		$this->reflectProperties();
		$this->reflectMethods();
		return $this->data;
	}

	/**
	 * Reflects the properties of the class.
	 *
	 * @uses MysqlModel::getProperties()
	 *
	 * @return void
	 */
	private function reflectProperties() {
		$properties = $this->reflection->getProperties();
		$props = array();
		$parentClass = $this->reflection->getParentClass();
		$parentClassName = '';
		if ($parentClass instanceof ReflectionClass) {
			$parentClassName = $parentClass->getName();
		}
		if (count($properties) == 0 || $parentClassName == 'MysqlModel') {
			$className = $this->className;
			$obj = new $className();
			if (method_exists($obj, 'getProperties')) {
				$props = $obj->getProperties();
				ksort($props);
			}
		} else {
			foreach ($properties as $property) {
				$name = $property->name;
				$comments = $property->getDocComment();
				$props[$name] = array('description' => '', 'type' => '', 'is_public' => false);
				if (strlen($comments) > 0) {
					$lines = explode("\n", $comments);
					$info = '';
					foreach ($lines as $line) {
						$line = trim($line);
						$line = substr($line, 2, strlen($line));
						if (substr($line, 0, 4) == '@var') {
							$props[$name]['type'] = trim(str_replace('@var', '', $line));
						} elseif ($line == '@exposePublic') {
							$props[$name]['is_public'] = true;
						} elseif (substr($line, 0, 1) == '@') {
							continue;
						} elseif ($line != '*') {
							$props[$name]['description'] .= trim($line) . "\n";
						}
					}
					$props[$name]['description'] = trim($props[$name]['description']);
				}
			}
		}
		ksort($props);
		$this->data['properties'] = $props;
	}

	/**
	 * Reflects the methods of the class.
	 *
	 * @return void
	 */
	private function reflectMethods() {
		$methods = $this->reflection->getMethods();
		$allMethods = array();
		foreach ($methods as $method) {
			$name = $method->name;
			$comments = $method->getDocComment();
			$allMethods[$name] = array('description' => '', 'signature' => '', 'parameters' => array(), 'return_type' => '', 'is_public' => false);
			if (strlen($comments) > 0) {
				$lines = explode("\n", $comments);
				$description = '';
				foreach ($lines as $line) {
					$line = trim($line);
					$line = substr($line, 2, strlen($line));
					if (substr($line, 0, 6) == '@param') {
						$params = explode(' ', $line);
						$paramTag = array_shift($params);
						$paramType = array_shift($params);
						$paramName = array_shift($params);
						$paramName = str_replace('$', '', $paramName);
						$paramDesc = implode(' ', $params);
						$allMethods[$name]['parameters'][$paramName] = array('type' => $paramType, 'description' => $paramDesc);
					} elseif (substr($line, 0, 7) == '@return') {
						$allMethods[$name]['return_type'] = trim(substr($line, 7, strlen($line)));
					} elseif ($line == '@exposePublic') {
						$allMethods[$name]['is_public'] = true;
					} elseif (substr($line, 0, 1) == '@') {
						continue;
					} elseif ($line != '*') {
						$allMethods[$name]['description'] .= trim($line) . "\n";
					}
				}
				$signature = $name . '(';
				$parameters = $method->getParameters();
				foreach ($parameters as $param) {
					$paramName = $param->name;
					$isOption = $param->isOptional();
					if ($isOption) {
						$signature .= "[";
					}
					if (isset($allMethods[$name]['parameters'][$paramName])) {
						$signature .= $allMethods[$name]['parameters'][$paramName]['type'] . ' ';
					} else {
						$signature .= 'Unknown ';
					}
					$signature .= $paramName;
					if ($param->isDefaultValueAvailable()) {
						$signature .= ' = "' . $param->getDefaultValue() . '"';
					}
					if ($isOption) {
						$signature .= ']';
					}
					$signature .= ', ';
				}
				$signature = substr($signature, 0, strlen($signature) - 2) . ')';
				$allMethods[$name]['signature'] = $signature;
				$allMethods[$name]['description'] = trim($allMethods[$name]['description']);
			}
		}
		ksort($allMethods);
		$this->data['methods'] = $allMethods;
	}

}
